-
I want static.
-
Maybe HTMX is overkill for that and requires me to think if I use templates or markup-as-code.
-
I can maybe run away from JS, but there's no running from HTML and CSS.
-
I need to learn HTML, regardless if I use Hugo, or Odin + HTMX or Odin + Templating, or something hybrid.
-
I imagine that if I want an easy front-end, I would go for templating, and therefore, Hugo.
-
If I want templating, why would I consider doing something myself, in Odin?
-
Hugo does things for me, but I don't like how opaque things can be, or how I have to follow the conventions defined by Hugo, without really knowing what happens.
-
So, Hugo hides the backend, and hides how the front-end is built, inside layers of documentation.
-
If I expose the backend with Odin, I could understand everything.
-
-
What would be needed, if I choose to make a backend in Odin:
-
HTTP routing.
-
Support for the templating used by Go.
-
Parsing of the Markdown file, so its used correctly by the templating service.
-
A way that I can open a server.
-
Could my
game_netlibrary do that? I think so.
-
-
-
I don't want to use WASM. I lose themes and convenience. Performance may not be optimal. Over complicates with Odin, while being opaque and with no front-end.
Template-in-HTML Engines
-
Uses declarative HTML files (or partials) with embedded logic (templating syntax).
Etc
HTMX
-
HTMX .
Alpine Ajax
Alpine.js
Jinja2 Family
{% for u in users %}
<li>{{ u }}</li>
{% endfor %}
Temple (Odin)
Go Templates
-
Server-side only, but outputs static HTML usable client-side.
-
Hugo uses it.
<ul>
{{ range .Pages }}
<li><a HREF="{{ .RelPermalink }}">{{ .Title }}</a></li>
{{ else }}
<li>No pages</li>
{{ end }}
</ul>
-
"Functionally similar to Jinja2 but in Go."
Plush (Go)
-
Mimics Jinja2.
Pongo2 (Go)
-
Server-side.
-
Mimics Jinja2.
Nunjucks (Node.js)
-
Client-side.
-
Mozilla’s Jinja2 port for Node.js.
const nunjucks = require("nunjucks");
const env = new nunjucks.Environment();
const template = `
<ul>
{% for u in users %}
<li>{{ u }}</li>
{% endfor %}
</ul>`;
console.log(env.renderString(template, { users: ["Alice", "Bob"] }));
Jinja2 (Flask/Django Template)
-
Server-side.
-
Original source of the style (
{% for %},{{ var }}).
# app.py
from jinja2 import Template
template = Template("""
<ul>
{% for u in users %}
<li>{{ u }}</li>
{% endfor %}
</ul>
""")
print(template.render(users=["Alice", "Bob"]))
Twig (PHP)
-
Server-side.
-
Direct port of Jinja2; nearly identical syntax.
<?php
require_once '/path/to/vendor/autoload.php';
$loader = new \Twig\Loader\ArrayLoader([
'list' => '<ul>{% for u in users %}<li>{{ u }}</li>{% endfor %}</ul>',
]);
$twig = new \Twig\Environment($loader);
echo $twig->render('list', ['users' => ['Alice', 'Bob']]);
Pebble (Java)
-
Server-side.
-
Jinja2-inspired syntax for Java.
Liquid Family
-
Liquid was directly inspired by Jinja2, but the design goals differ.
-
Main distinction: Liquid is safe and restricted, Jinja2 is powerful and programmable.
{% for u in users %}
<li>{{ u.name }}</li>
{% endfor %}
Liquid (Go)
-
Server-side.
-
Port with near-identical tags.
package main
import (
"fmt"
"github.com/osteele/liquid"
)
func main() {
tmpl := liquid.NewTemplate(`
<ul>
{% for u in users %}
<li>{{ u }}</li>
{% endfor %}
</ul>`)
out, _ := tmpl.Render(map[string][]string{"users": {"Alice", "Bob"}})
fmt.Println(out)
}
Liquid (Ruby)
-
Server-side.
-
Canonical version.
Liquid-php (PHP)
-
Server-side.
-
Syntax and filters same as Ruby’s version.
Mustache Family
<ul>
{{#users}}
<li>{{name}}</li>
{{/users}}
</ul>
odin-mustache (Odin)
Mustache (Go - hoisie/mustache)
-
Server-side.
package main
import (
"fmt"
"github.com/hoisie/mustache"
)
func main() {
tpl := "<ul>{{#users}}<li>{{.}}</li>{{/users}}</ul>"
html := mustache.Render(tpl, map[string][]string{"users": {"Alice", "Bob"}})
fmt.Println(html)
}
Mustache (Go - broglie/mustache)
-
Server-side.
Handlebars (JavaScript Node.js)
-
Client-side.
const Handlebars = require("handlebars");
const source = `
<ul>
{{#each users}}
<li>{{this}}</li>
{{/each}}
</ul>`;
const template = Handlebars.compile(source);
console.log(template({ users: ["Alice", "Bob"] }));
Mustache.js (JavaScript)
-
Client-side.
-
Logic-less templates.
pystache (Python)
-
Server-side.
-
Compatible implementation.
mustache.java (Java)
-
Server-side.
-
Official Java port.
mustache.php (PHP)
-
Server-side.
-
Official PHP port.
mustache.rb (Ruby)
-
Server-side.
-
Original reference version.
Embedded Code Families
<ul>
<% users.each do |u| %>
<li><%= u %></li>
<% end %>
</ul>
-
Pattern
<% code %>and<%= output %>.
EJS (Node.js)
-
Client-side.
// index.js
const ejs = require("ejs");
const template = `
<ul>
<% users.forEach(u => { %>
<li><%= u %></li>
<% }) %>
</ul>`;
const html = ejs.render(template, { users: ["Alice", "Bob"] });
console.log(html);
ERB (Ruby on Rails)
-
Server-side.
require "erb"
template = <<~ERB
<ul>
<% users.each do |u| %>
<li><%= u %></li>
<% end %>
</ul>
ERB
puts ERB.new(template).result_with_hash(users: ["Alice", "Bob"])
PHP (native templates)
<ul>
<?php if (!empty($items)): ?>
<?php foreach ($items as $item): ?>
<li><?= htmlspecialchars($item['title']) ?></li>
<?php endforeach; ?>
<?php else: ?>
<li>No items</li>
<?php endif; ?>
</ul>
JSP / FreeMarker (Java)
-
Server-side.
<ul>
<% String[] users = {"Alice", "Bob"}; %>
<% for (String u : users) { %>
<li><%= u %></li>
<% } %>
</ul>
Markup-as-Code (DSL for HTML)
-
All UI layout and structure are defined through code (e.g., a DSL or API calls) instead of static HTML templates.
-
"I'm a senior programmer in C and Odin Lang. Which strategy would suit me best as a web developer?"
-
Pick Markup-as-Code + strong typing.
-
Reason: you value explicit control, deterministic behavior, and compile-time checks. Markup-as-Code maps to those priorities better than template HTML.
-
Types and compiler safety mirror C/Odin workflows.
-
Programmatic UI lets you express invariants, abstractions, and reusable low-level primitives.
-
Easier to test, refactor, and profile.
-
Better fit for complex state and performance tuning.
-
-
"Now, also consider that I hate JavaScript. Would your suggestion change?"
-
Yes. Avoiding JavaScript shifts the balance. You’ll favor Markup-as-Code in a non-JS ecosystem, ideally with a compiled language that generates web UI.
-
Templ (Go)
-
Type-safe HTML template for Go.
-
Compile-time type-safe.
-
-
Component Model.
-
Just like React, Vue.
-
-
JSX-like, but pure Go. Generates type-safe HTML components compiled to Go code.
-
Output: plain HTML at compile time. No JavaScript.
package main
import "github.com/a-h/templ"
var userList = templ.Component(func(users []User) templ.Component {
return templ.Ul(
templ.Range(users, func(u User) templ.Component {
return templ.Li(templ.Text(u.Name))
}),
)
})
-
To use, you create a file with
.templ, which is written just like any other Go file.
html/template (Go)
-
Runtime Type-safe.
-
Seems like I should simply go for Templ instead, just because of this.
-
-
Interpreted at runtime (slower).
-
Less component structured.
-
Demo .
JS/TS-based
JSX
-
JSX does not strictly require React, but it originated there.
-
It is a syntax , not a framework.
-
You can use JSX with other libraries if they define a
createElementfunction or JSX pragma, for example:-
Preact (
hpragma) -
Solid.js
-
Nerv
-
Qwik
-
Custom renderers
-
-
JSX is a syntax extension that allows writing HTML-like elements directly in JavaScript/TypeScript code.
-
In a
.tsxfile, the TypeScript compiler translates JSX into function calls (likeReact.createElement) or into the JSX syntax expected by the framework in use. -
With React-style :
import React from 'react'; const App: React.FC = () => { return ( <div> <h1>Hello, TypeScript with JSX!</h1> </div> ); }; export default App;export default function List({ items }) { if (!items.length) return <div>No items</div>; return ( <ul> {items.map(item => <li key={item.id}>{item.title}</li>)} </ul> ); }
Qwik
// UserList.qwik.tsx
import { component$ } from '@builder.io/qwik';
export const UserList = component$((props: { users: { id:number, name:string }[] }) => {
return (
<ul>
{props.users.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
);
});
Angular
-
Template syntax, but bound tightly to TypeScript logic.
Lit / Web Components
import { LitElement, html } from 'lit';
class UserList extends LitElement {
static properties = { users: { type: Array } };
constructor() { super(); this.users = []; }
render() {
return html`<ul>${this.users.map(u => html`<li>${u.name}</li>`)}</ul>`;
}
}
customElements.define('user-list', UserList);
Preact
-
JSX, lightweight React alternative.
Solid
Vue
Svelte
Based on bad languages
Yew (Rust)
-
React-like, with macro-based templating.
-
Client-side via WebAssembly (SPA-style)
-
Performance lighter, Solid.js-like
-
JSX-like macros
-
Full SPA in WASM
use leptos::*;
#[component]
pub fn UserList(cx: Scope, users: Vec<String>) -> impl IntoView {
view! { cx,
<ul>
{users.into_iter().map(|u| view!{ cx, <li>{u}</li> }).collect::<Vec<_>>()}
</ul>
}
}
Leptos (Rust)
-
Reactive, component-based, JSX-style but all Rust.
-
Compiles to WebAssembly.
-
Hybrid: server-side rendering + hydration (like Next.js)
-
Performance heavier, React-like.
-
Reactive SSR or hybrid web app
html! {
<ul>
{ for users.iter().map(|u| html!{ <li>{ &u.name }</li> }) }
</ul>
}
Pynecone / Reflex (Python)
import reflex as rx
def users_view(users):
return rx.ul(rx.foreach(users, lambda u: rx.li(u.name)))
NiceGUI (Python)
-
Python API generating web UIs with no HTML coding.
Pug (Node.js)
-
Formerly Jade.
-
Write HTML with indentation-based syntax instead of angle brackets.
const pug = require("pug");
const template = `
ul
each u in users
li= u
`;
const html = pug.render(template, { users: ["Alice", "Bob"] });
console.log(html);
Flutter (Dart)
-
Widget tree is code.
ListView(
children: users.map((u) => Text(u.name)).toList(),
);
SwiftUI (Swift)
List(users) { user in
Text(user.name)
}
Jetpack Compose (Kotlin)
LazyColumn {
items(users) { user ->
Text(user.name)
}
}
Blazor (C#)
-
With Razor components.
<ul>
@foreach (var user in Users)
{
<li>@user.Name</li>
}
</ul>